#region Copyright (c) 2007-2008, Chris Klochek

/********************************************************************************************************************
'
' Copyright (c) 2007-2008, Chris Klochek
' All rights reserved.
'
' Redistribution and use in source and binary forms, with or without modification, are permitted provided
' that the following conditions are met:
'
' * Redistributions of source code must retain the above copyright notice, this list of conditions and the
' 	following disclaimer.
'
' * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
' 	the following disclaimer in the documentation and/or other materials provided with the distribution.
'
' * Neither the name of the author nor the names of its contributors may be used to endorse or
' 	promote products derived from this software without specific prior written permission.
'
' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
' WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
' PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
' ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
' LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
' INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
' OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
' IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'
'*******************************************************************************************************************/

#endregion
using System;
using System.Collections.Generic;
using System.Text;
using Majestic12;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32;
using System.Reflection;
using System.Windows.Forms;
using System.IO;


namespace SuedeTest.Core {
   public class AssertFailedException : Exception {
      public AssertFailedException(string msg)
         : base(msg) {

      }
   }

   /// <summary>
   /// Registry of actions that can be performed (e.g. "Select", "Click", "Enter"),
   /// and delegates that are registered to perform such actions (e.g., A combobox can
   /// perform a Select, and so can a MenuItem.)
   /// The fact that there is a one->many relationship between actions and delegates
   /// implies we need a registry so that we can try all possibilities (test-scripts do
   /// not contain type-information, by design.)
   /// </summary>
   public class ActionDelegateRegistry {

      /// <summary>
      /// Convenience class to associate delegates with constructors.
      /// </summary>
      public class ConstructorDelegatePair {
         public ConstructorDelegatePair(MethodInfo ad, ConstructorInfo ctor) {
            actionDelegate = ad;
            typeConstructor = ctor;
         }
         public MethodInfo actionDelegate;
         public ConstructorInfo typeConstructor;
      }

      private System.Collections.Hashtable _actionDelegatesMap = new System.Collections.Hashtable();

      /// <summary>
      /// Binds an ActionDelegate + ConstructorInfo to an action name.
      /// </summary>
      /// <param name="actionName"></param>
      /// <param name="del"></param>
      /// <param name="ctor"></param>
      public void AddActionDelegate(string actionName, MethodInfo del, ConstructorInfo ctor) {
         if (_actionDelegatesMap.Contains(actionName)) {
            ((List<ConstructorDelegatePair>)_actionDelegatesMap[actionName]).Add(new ConstructorDelegatePair(del, ctor));
         } else {
            _actionDelegatesMap.Add(actionName, new List<ConstructorDelegatePair>(new ConstructorDelegatePair[] { new ConstructorDelegatePair(del, ctor) }));
         }
      }

      /// <summary>
      /// Returns the list of ActionDelegates/Constructors bound to the action name; null
      /// if no such element exists.
      /// </summary>
      /// <param name="actionName"></param>
      /// <returns></returns>
      public List<ConstructorDelegatePair> this[string actionName] {
         get {
            return _actionDelegatesMap[actionName] as List<ConstructorDelegatePair>;
         }
      }

      /// <summary>
      /// Constructs the ActionDelegateRegistry.  It goes through all loaded Assemblies, to
      /// look for types that are subclasses of ReflectionTesters (these are potential
      /// action-makers), and add any methods tagged as RunnableAttribute as an
      /// ActionDelegate.
      /// </summary>
      public ActionDelegateRegistry() {
         List<string> asmMask = new List<string>(new string[] { "System",
            "System.Windows.Forms", "mscorlib", "System.Drawing",
            "SuedeTest.ProcessHost", "SuedeTest.VSPlugin", "SuedeTest.FormExplorer" });

         foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) {
            if (asmMask.Contains(assembly.FullName.Substring(0, assembly.FullName.IndexOf(","))))
               continue;
            try {
               foreach (Type type in assembly.GetTypes()) {
                  if (type.IsSubclassOf(typeof(ReflectionTester)) && !type.IsAbstract) {
                     AddActionToRegistry(type);
                  }
               }
            } catch (ReflectionTypeLoadException) {
            }
         }
      }

      /// <summary>
      /// Go through all the methods of the associated type; if a method is tagged with a
      /// RunnableAttribute attribute, then it is meant to be used as an ActionDelegate
      /// </summary>
      /// <param name="type"></param>
      private void AddActionToRegistry(Type type) {
         foreach (MethodInfo mi in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic)) {
            object[] attributes = mi.GetCustomAttributes(typeof(RunnableAttribute), false);
            if (attributes != null && attributes.Length > 0) {

               ConstructorInfo ctor = mi.DeclaringType.GetConstructor(new Type[] { typeof(string) });

               AddActionDelegate(((RunnableAttribute)attributes[0]).ActionName,
                  mi, ctor);
            }
         }
      }
   }

   /// <summary>
   /// An entire "program" of unit-tests.  Contains multiple individual test-forms.
   /// </summary>
   public class TestProgram {
      private List<TestFrame> _testFrames;
      private ActionDelegateRegistry _actionRegistry;

      public static event EventHandler TestOutputChanged;

      public ActionDelegateRegistry ActionRegistry {
         get {
            return _actionRegistry;
         }
      }

      #region HTML Parsing
      public List<List<List<string>>> ParseFile(string fileName) {
         List<List<List<string>>> script = new List<List<List<string>>>();
         HTMLparser parser = new HTMLparser();
         parser.LoadFromFile(fileName);

         HTMLchunk tableStart = NextTable(parser);
         while (tableStart != null) {
            script.Add(ParseTable(parser, tableStart));
            tableStart = NextTable(parser);
         }

         return script;
      }

      private List<List<string>> ParseTable(HTMLparser parser, HTMLchunk chunk) {
         //got here via tbody, so next chunk should be a tr
         List<List<string>> table = new List<List<string>>();
         chunk = parser.ParseNextTag();
         while ((chunk.oType != HTMLchunkType.CloseTag) || (chunk.sTag != "table")) {
            table.Add(ParseRow(parser));
            chunk = parser.ParseNextTag();
         }

         return table;
      }

      private List<string> ParseRow(HTMLparser parser) {
         //got here via tr, so next chunk should be a td
         List<string> row = new List<string>();

         HTMLchunk chunk = parser.ParseNextTag();

         while ((chunk.oType != HTMLchunkType.CloseTag) || (chunk.sTag != "tr")) {
            row.Add(ParseCell(parser));
            chunk = parser.ParseNextTag();
         }

         return row;
      }

      private string ParseCell(HTMLparser parser) {
         //got here via td, so we should have data, followed by /td

         HTMLchunk chunk = parser.ParseNext();
         string val = "";
         while (chunk.oType != HTMLchunkType.CloseTag || chunk.sTag != "td") {
            val += chunk.oHTML;
            chunk = parser.ParseNext();
         }

         return val.Trim();
      }

      private HTMLchunk NextTable(HTMLparser parser) {
         HTMLchunk chunk = parser.ParseNextTag();

         while (chunk != null && chunk.sTag != "table") {
            chunk = parser.ParseNext();
         }

         return chunk;
      }

      #endregion

      internal class ListViewWatchHandler : ReflectionTester {
         private struct SubItemProperties {
            public int subItemIndex;
            public string property;
            public string value;
         }

         private string _listName;
         private SubItemProperties[] _properties;

         public ListViewWatchHandler(string args) {
            string[] properties = args.Split(new string[] { System.Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
            _listName = properties[0];

            _properties = new SubItemProperties[properties.Length - 1];

            for (int x = 0; x < properties.Length - 1; x++) {
               string[] subProps = properties[x + 1].Split(';');

               _properties[x].property = subProps[0].Substring(subProps[0].IndexOf('.') + 1);
               _properties[x].subItemIndex = int.Parse(subProps[0].Substring(subProps[0].IndexOf('[') + 1, subProps[0].IndexOf(']') - subProps[0].IndexOf('[') - 1));
               _properties[x].value = subProps[1];
            }
         }

         [Runnable("ListViewWatch")]
         internal void WatchForListItem(string args) {
            ListViewTester lt = new ListViewTester(_listName);


            foreach (ListViewItem lvi in lt.TheObject.Items) {

               bool found = true;

               for (int x = 0; x < _properties.Length && found; x++) {
                  ListViewItem.ListViewSubItem lvsi = lvi.SubItems[_properties[x].subItemIndex];

                  string subItemValue = (string)lvsi.GetType().GetProperty(_properties[x].property).GetValue(lvsi, null);

                  if (!subItemValue.Equals(_properties[x].value)) {
                     //if any item does not match, break from the loop
                     found = false;
                  }
               }

               if (found) {
                  WriteStringToOutput("ListViewItem on " + _listName + " appeared.");
                  return;
               }
            }

            throw new NoSuchControlException("Could not find matching ListViewItem");
         }
      }


      /// <summary>
      /// Internal class to be created as a sink for asserts coming from the test
      /// runner.  Using this class, we can employ the main test-runner loop and action-
      /// delegate logic for us in constructing and evaluating an assert.
      /// </summary>
      internal class AssertHandler : ReflectionTester {
         private string _propertyPath;

         public AssertHandler(string propertyPath) {
            _propertyPath = propertyPath;
         }

         /// <summary>
         /// Perform a check against the value of a property of an object, and a
         /// previously-seen value.
         /// </summary>
         /// <param name="o">The AssertHandler containing the object's property-path</param>
         /// <param name="args">The value to compare against</param>
         [Runnable("Assert_AreEqual")]
         internal void Assert_AreEqual(string args) {
            //split the string in two, to get the namespace.object and any property path associated with it
            //eg, Foo.Bar.ListView+Items[0].SubItems[0].Text

            string[] temp = (_propertyPath).Split('+');

            string objName = temp[0];
            string[] propertyPath = temp[1].Split('.');

            ControlTester ct = new ControlTester(objName);

            string val = RetrievePropertyFromControl(ct.TheObject, propertyPath);
            if (val == args) {
               WriteStringToOutput("Assertion Passed." + System.Environment.NewLine);
            } else {
               throw new AssertFailedException("Assertion Failed\r\nControl Name : " + _propertyPath + "\r\nExpected Value : " + args + "\r\nGot : " + val + "\r\n");
               //WriteErrorStringToOutput();
            }
         }

         /// <summary>
         /// Returns the value (converted to string) of a property on a control.
         /// </summary>
         /// <param name="ctrl">The control to retrieve the property from.</param>
         /// <param name="propertyPath">The property to be retrieved.  e.g. "Text",
         /// or "Items[0].SubItems[3].Text"</param>
         /// <returns>The value of the property, as a string, if the property is correctly
         /// found.  Empty-string, otherwise.</returns>
         private static string RetrievePropertyFromControl(Control ctrl, string[] propertyPath) {
            Type currentType = ctrl.GetType();
            object currentObj = ctrl;
            try {
               foreach (string property in propertyPath) {
                  if (IsArrayProperty(property)) {
                     string indexString = property.Substring(property.IndexOf('[') + 1).Trim(']');

                     int index;
                     int.TryParse(indexString, out index);
                     string propName = property.Substring(0, property.IndexOf('['));

                     currentObj = currentType.GetProperty(propName).GetValue(currentObj, null);
                     currentType = currentObj.GetType();

                     currentObj = currentType.GetProperty("Item", new Type[] { typeof(Int32) }).GetValue(currentObj, new object[] { index });

                  } else {
                     currentObj = currentType.GetProperty(property).GetValue(currentObj, null);
                  }
                  currentType = currentObj.GetType();
               }
            } catch {
               return "";
            }

            if (currentObj != null) {
               return currentObj.ToString();
            }
            return "";
         }

         /// <summary>
         /// Returns true iff the property is an array.
         /// </summary>
         /// <param name="property"></param>
         /// <returns></returns>
         private static bool IsArrayProperty(string property) {
            return property.Contains("]") && property.Contains("[");
         }
      }

      /// <summary>
      /// Little internal class to be created as a sink for waits coming from the test
      /// runner.  Using this class, we can employ the main test-runner loop and action-
      /// delegate logic for us in performing a wait-operation.
      /// </summary>
      internal class WaitHandler : ReflectionTester {
         public WaitHandler(string s) {

         }

         /// <summary>
         /// Wait for a period of time (specified in args as seconds.)  If arg cannot be
         /// parsed, then wait for 5 seconds.
         /// </summary>
         /// <param name="o"></param>
         /// <param name="args"></param>
         [Runnable("Wait")]
         internal void WaitForTime(string args) {
            int time;
            if (!int.TryParse(args, out time)) {
               time = 5000;
            }

            Thread.Sleep(time);
         }
      }

      internal class MessageBoxHandler : ReflectionTester {
         string _controlName;
         public MessageBoxHandler(string s) {
            _controlName = s;
         }

         [Runnable("DIALOG_&Yes")]
         internal void MessageBoxYes(string args) {
            MessageBoxTester mbt = new MessageBoxTester(_controlName);

            mbt.ClickYes();
         }

         [Runnable("DIALOG_OK")]
         internal void MessageBoxOk(string args) {
            MessageBoxTester mbt = new MessageBoxTester(_controlName);

            mbt.ClickOk();
         }
      }

      /// <summary>
      /// Construct a test-program from the .html filename "scriptFile"
      /// </summary>
      /// <param name="scriptFile"></param>
      public TestProgram(string scriptFile, ActionDelegateRegistry adr) {
         _actionRegistry = adr;
         List<List<List<string>>> testFrameStatements = ParseFile(scriptFile);

         _testFrames = new List<TestFrame>();
         foreach (List<List<string>> frame in testFrameStatements) {
            TestFrame tf = new TestFrame(this, frame[0][0], frame);
            _testFrames.Add(tf);
         }
      }

      /// <summary>
      /// Construct a test-program from the .html filename "scriptFile".  Replace any
      /// constants in the scriptFile with values from the file "specFile".
      /// </summary>
      /// <param name="scriptFile"></param>
      public TestProgram(string scriptFile, string specFile, ActionDelegateRegistry adr) {
         _actionRegistry = adr;
         List<List<List<string>>> testFrameStatements = ParseFile(scriptFile);

         ReplaceParameters(testFrameStatements, specFile);

         _testFrames = new List<TestFrame>();
         foreach (List<List<string>> frame in testFrameStatements) {
            TestFrame tf = new TestFrame(this, frame[0][0], frame);
            _testFrames.Add(tf);
         }
      }

      /// <summary>
      /// Writes a string to the IPC Pipe that's handling this process.
      /// </summary>
      /// <param name="s"></param>
      public static void WriteStringToOutput(string s) {
         if (TestOutputChanged != null) {
            TestOutputChanged(s + System.Environment.NewLine, null);
         }
      }

      /// <summary>
      /// Replaces all occurrences of the keywords found in the file 'specfile' with the values
      /// specified in 'specfile' in the data-fields of the supplied set of tests.  Note that
      /// this is a simple search/replace, so no keyword should be the proper prefix of another.
      /// </summary>
      /// <param name="file"></param>
      /// <param name="specFile"></param>
      /// <example>specfile: MYNAME = Christopher
      ///                    MYSTATE = California</example>
      private void ReplaceParameters(List<List<List<string>>> file, string specFile) {
         if (File.Exists(specFile)) {
            List<KeyValuePair<string, string>> specs = new List<KeyValuePair<string, string>>();

            StreamReader reader = new StreamReader(specFile);

            while (!reader.EndOfStream) {
               string line = reader.ReadLine();

               if (line.IndexOf('=') > 0) {
                  string[] keyValue = line.Split('=');

                  specs.Add(new KeyValuePair<string, string>(keyValue[0].Trim(), keyValue[1].Trim()));
               }
            }

            reader.Close();
            reader.Dispose();
            foreach (List<List<string>> test in file) {
               foreach (List<string> row in test) {
                  if (row.Count > 2) {
                     for (int x = 0; x < specs.Count; x++) {
                        if (row[2].Contains(specs[x].Key)) {
                           row[2] = row[2].Replace(specs[x].Key, specs[x].Value);
                        }
                     }
                  }
               }
            }
         }
      }

      /// <summary>
      /// Get an individual TestFrame via its name.
      /// </summary>
      /// <param name="name"></param>
      /// <returns></returns>
      public TestFrame GetTestFrame(string name) {
         foreach (TestFrame frame in _testFrames) {
            if (frame.Name == name) {
               return frame;
            }
         }
         return null;
      }

      /// <summary>
      /// Returns the list of test-frames associated with this test program
      /// </summary>
      public List<TestFrame> TestFrames {
         get {
            return _testFrames;
         }
      }
   }

   /// <summary>
   /// A test-frame--a sequence of instructions to run against SuedeTest.
   /// </summary>
   public class TestFrame {
      private TestProgram _runningProgram;
      private string _frameName;
      private List<List<string>> _statements;

      private const int DEFAULT_CONTROL_WAIT_TIME = 30;
      private const int POLL_TIME = 500;

      private const int CONTROL = 0;
      private const int METHOD = 1;
      private const int ARGS = 2;
      private int ip = 0;

      /// <summary>
      /// Returns the name of this test-frame.
      /// </summary>
      public string Name {
         get { return _frameName; }
      }

      /// <summary>
      /// Constructs a test-frame given a TestProgram owner, the name of the test-frame,
      /// and a list of instructions to execute.
      /// </summary>
      /// <param name="owner"></param>
      /// <param name="name"></param>
      /// <param name="statements"></param>
      public TestFrame(TestProgram owner, string name, List<List<string>> statements) {
         _frameName = name;
         _runningProgram = owner;
         _statements = statements;
         ip = 1;  //start us after the name of the test
      }

      /// <summary>
      /// Trys to successfully run all possible actions on an action name against a control
      /// and with supplied arguments.  This method times out if the action cannot be
      /// performed because the control was not found.
      /// </summary>
      /// <param name="possibleActions"></param>
      /// <param name="statementControlName"></param>
      /// <param name="statementArgs"></param>
      /// <returns></returns>
      private bool RunAllActions(List<ActionDelegateRegistry.ConstructorDelegatePair> possibleActions, string statementControlName, string statementArgs) {
         bool found = false;
         DateTime start = DateTime.Now;
         DateTime current = DateTime.Now;
         Exception error = null;

         TestProgram.WriteStringToOutput("" + (ip - 1) + ": " + statementControlName + ", " + statementArgs);

         while (!found && (current.Subtract(start).TotalSeconds < DEFAULT_CONTROL_WAIT_TIME)) {
            error = null;

            for (int x = 0; x < possibleActions.Count && !found; x++) {
               MethodInfo runnerAction = possibleActions[x].actionDelegate;
               ConstructorInfo typeConstructor = possibleActions[x].typeConstructor;

               object o = null;
               if (typeConstructor != null) {
                  try {
                     try {
                        o = typeConstructor.Invoke(new object[] { statementControlName });
                        runnerAction.Invoke(o, new object[] { statementArgs });
                        found = true;
                     } catch (TargetInvocationException te) {
                        if (te.InnerException != null) {
                           throw te.InnerException;
                        }
                        TestProgram.WriteStringToOutput(te.ToString());
                        return false;
                     } catch (Exception e) {
                        throw e;
                     }
                  } catch (NoSuchControlException e) {
                     error = e;
                  } catch (ControlNotVisibleException e) {
                     error = e;
                  } catch (FormatException) {

                  } catch (AmbiguousNameException) {
                     //fatal, exit immediately
                     TestProgram.WriteStringToOutput("Ambiguous control name.");
                     return false;
                  } catch (AssertFailedException e) {

                     //we might just be waiting for a control's property to change
                     error = e;
                  } catch (InvokeTimeoutException) {
                     TestProgram.WriteStringToOutput("Aborting test: UI action took too long to complete.");
                     //fatal, exit immediately
                     return false;
                  } catch (Exception e) {
                     string expText = e.ToString();
                     if (e.InnerException != null) {
                        expText += Environment.NewLine + e.InnerException.ToString();
                     }
                     TestProgram.WriteStringToOutput(expText);

                     return false;
                  }
               }
            }

            if (!found) {
               Thread.Sleep(POLL_TIME);
            }

            current = DateTime.Now;
         }

         if (!found) {
            TestProgram.WriteStringToOutput(error.Message + Environment.NewLine + (error.InnerException == null ? "" : error.InnerException.ToString()));
         }

         return found;
      }

      //Run this test-frame's test.
      public bool RunScript() {
         bool result = true;
         ActionDelegateRegistry actionRegistry = _runningProgram.ActionRegistry;
         TestProgram.WriteStringToOutput(Environment.NewLine + "Starting script " + _statements[0][0]);

         while (ip < _statements.Count) {
            List<string> statement;

            statement = _statements[ip];
            ip++;

            List<ActionDelegateRegistry.ConstructorDelegatePair> possibleActions = actionRegistry[statement[METHOD]];

            if (possibleActions != null) {
               result = RunAllActions(possibleActions, statement[CONTROL], statement[ARGS]);

               if (!result) {
                  TestProgram.WriteStringToOutput("Terminating script....");
                  ip = _statements.Count;
               }
            }
         }
         TestProgram.WriteStringToOutput("Script completed.");

         return result;
      }
   }
}
